Skip to content

feat(ascend): add Device injection policy and fix A5 driver/UB mounts - #25

Closed
yxf0314 wants to merge 1 commit into
gpustack:mainfrom
yxf0314:issue/6148-3
Closed

feat(ascend): add Device injection policy and fix A5 driver/UB mounts#25
yxf0314 wants to merge 1 commit into
gpustack:mainfrom
yxf0314:issue/6148-3

Conversation

@yxf0314

@yxf0314 yxf0314 commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

On Docker, the default Env injection policy sets the visible-devices env (ASCEND_VISIBLE_DEVICES), which makes the Ascend Docker Runtime apply device isolation. On A5 (950) that isolation hides the UB fabric, so HCCL rootInfo detection fails. The CDI mount profile, modeled on the A2/A3 operator profile, also omits A5's UB driver components (ube_mgmt, under the driver tree) and blanket-mounts a host /etc/hccl_rootinfo.json whose stale content fails rootInfo detection.

  • Add a "Device" resource injection policy: inject device nodes and mounts as plain Docker devices/binds (reusing the CDI generator), with no visible-devices env and without requiring CDI-capable Docker.
  • Drop mirrored visible-devices envs under non-Env policies so a mirrored ASCEND_VISIBLE_DEVICES cannot re-trigger the isolation.
  • For A5, mount the whole /usr/local/Ascend/driver (brings ube_mgmt) and stop mounting /etc/hccl_rootinfo.json.
  • Keep injected mounts when appending container mounts, and fix a device mirroring typo exposed by the new plain-device path.

On Docker, the default Env injection policy sets the visible-devices env
(ASCEND_VISIBLE_DEVICES), which makes the Ascend Docker Runtime apply device
isolation. On A5 (950) that isolation hides the UB fabric, so HCCL rootInfo
detection fails. The CDI mount profile, modeled on the A2/A3 operator profile,
also omits A5's UB driver components (ube_mgmt, under the driver tree) and
blanket-mounts a host /etc/hccl_rootinfo.json whose stale content fails
rootInfo detection.

- Add a "Device" resource injection policy: inject device nodes and mounts as
  plain Docker devices/binds (reusing the CDI generator), with no visible-devices
  env and without requiring CDI-capable Docker.
- Drop mirrored visible-devices envs under non-Env policies so a mirrored
  ASCEND_VISIBLE_DEVICES cannot re-trigger the isolation.
- For A5, mount the whole /usr/local/Ascend/driver (brings ube_mgmt) and stop
  mounting /etc/hccl_rootinfo.json.
- Keep injected mounts when appending container mounts, and fix a device
  mirroring typo exposed by the new plain-device path.
@yxf0314
yxf0314 requested a review from thxCode September 3, 2026 15:57

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new 'Device' resource injection policy for the Docker deployer, allowing direct injection of device nodes and mounts to bypass the visible-devices environment variable, which resolves issues with the Ascend A5 UB fabric. It also refactors resource parsing and updates Ascend CDI configuration generation to mount the entire driver directory for A5 devices. The review feedback correctly identifies a critical bug in _inject_devices_plain where host and container paths are inverted when constructing the Docker device mapping string, and provides a code suggestion to fix it.

Comment on lines +860 to +866
for dn in device_nodes:
if dn.path in seen:
continue
seen.add(dn.path)
devices.append(
f"{dn.host_path or dn.path}:{dn.path}:{dn.permissions or 'rwm'}",
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

In device_to_cdi_device_node (defined in gpustack_runtime/deployer/cdi/__utils__.py), the parameters are passed to ConfigDeviceNode as ConfigDeviceNode(path=dev.path, host_path=container_path). This means that for any dn (a ConfigDeviceNode):

  • dn.path actually holds the host path.
  • dn.host_path actually holds the container path.

As a result, constructing the Docker device mapping string as f"{dn.host_path or dn.path}:{dn.path}" incorrectly maps container_path:host_path instead of host_path:container_path. This inversion will cause Docker to fail to mount the device correctly or fail to start the container.

Additionally, the duplicate check if dn.path in seen: compares the host path against container paths in seen (since seen extracts the container path from existing devices).

We should fix this by correctly resolving the container path and host path, and checking/adding the container path in seen.

Suggested change
for dn in device_nodes:
if dn.path in seen:
continue
seen.add(dn.path)
devices.append(
f"{dn.host_path or dn.path}:{dn.path}:{dn.permissions or 'rwm'}",
)
for dn in device_nodes:
container_path = dn.host_path or dn.path
if container_path in seen:
continue
seen.add(container_path)
devices.append(
f"{dn.path}:{container_path}:{dn.permissions or 'rwm'}",
)

@thxCode

thxCode commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Verified on a real 8× Ascend950PR host (card-4p, driver 25.7.rc1.6, ascend-docker-runtime v26.1.0,
CANN 9.1), with the machine otherwise idle. Your diagnosis is right and the fix works — but the
mechanism is not the one the PR states, and that changes how it should be framed.

How this PR actually fixes it, and why it needs both halves

The failure chain I measured:

  1. ascend-docker-runtime mounts /etc/hccl_rootinfo.json whenever the host has it — addUBMount
    is an os.Stat with no version check.
  2. A5 loads libhccl_v2.so, which demands a 2.0 ranktable. The file on this host is 1.0.
  3. Config_Error_Ranktable(EI0014), multi-card HCCL never initialises, TP=8 dies at startup.

Ranktable format is bound to the chip generation — 1.0 = A2, 1.2 = A3, 2.0 = A5 — so a table left
over from an older fleet is refused rather than ignored. I enumerated every HCCL_*/RANK_*/
ASCEND_* string in libhccl_v2.so: nothing disables the check, RANK_TABLE_FILE is not among
them. Not mounting the file is the only lever.

Your PR breaks that chain in two places and needs both:

  • (a) the Device policy — with no visible-devices env the vendor hook early-returns
    (hook/process/process.go:452-454), so it mounts nothing at all, ranktable included.
  • (c), second half — dropping /etc/hccl_rootinfo.json from GPUStack's own mount list
    (cdi/ascend.py:129). Without it the Device path re-mounts the very file (a) just avoided.
    Measured: adding only that file back to an otherwise-working container reproduces EI0014 exactly.

Each half is load-bearing. Worth stating in the description — right now the two read as unrelated.

But there is a smaller fix, and I think it should reframe the PR

I moved the host file aside and re-ran everything against unmodified gpustack-runtime
0.2.4.post1 (confirmed not to contain this PR), default Env policy:

configuration 8-card HCCL allreduce
--privileged + ASCEND_VISIBLE_DEVICES=0..7 — your own shape works
--privileged, no visible-devices env works
no privileged, env only works

Then end-to-end through gpustack-runtime create with your exact argument list
(--tensor-parallel-size=8 --max-model-len=131072 --speculative-config {"method":"mtp","num_speculative_tokens":5},
glm47/glm45 parsers): GLM-5.3-Flash started, MTP speculative decoding active, served a real
completion, EI0014 count 0. Same for a 4-card partial allocation on cards 4–7, unprivileged.

The file is user-maintained state: rpm -qf says it belongs to no package, the driver installed
two weeks before it appeared, and the vendor documents it as "rootinfo file generated by
mindcluster-tools. This file is optional"
, mounted when present on the host. So the operating
rule is simply: either absent, or a 2.0 table.

That does not make this PR pointless — it makes it defence rather than repair. It stops a
node's leftover ranktable from silently killing every multi-card workload. I'd suggest saying that,
because "required to run glm-5.3-flash" is no longer accurate and will mislead whoever revisits this.

The one change I'd ask you to drop: mounting the whole /usr/local/Ascend/driver

driver/ube_mgmt/ on this host is a single zero-byte file (upgrade_ube_mgmt_lock, 0600) — a
staging dir for upgrade-tool --upgrade_ube_mgmt_pack. No library under driver/lib64/ references
it, and ube_mgmt appears zero times in all of mind-cluster. The real UB code
(libascend_hal.so, 15 urma symbols; libibv_extend.so*) is already inside the lib64 you mount
today — and HCCL allreduces fine without the whole-tree mount.

The cost is real: it also hands the container driver/tools/ (upgrade-tool, hccn_tool, both
0555, beside ~120 MB of firmware images) into a container that already holds
/dev/davinci_manager, plus driver/cert/ca and 2570 kernel sources, while docker.py:875-882
drops nosuid/nodev. The vendor's named list (profile.go:83-90) is narrower, and its only A5
delta goes the other way: 950 drops /var/queue_schedule.

Two more, both worth keeping

Dropping mirrored visible-devices envs is a containment boundary. Granted davinci0,3 plus a
mirrored ASCEND_VISIBLE_DEVICES=5 → the container sees davinci0,3,5, device_count()==3. The
runtime adds to the device set. Not hypothetical: the GPUStack server container here carries
ASCEND_VISIBLE_DEVICES=0, so any workload not granted device 0 silently gains it.

privileged voids the isolation entirely — not covered by the PR. Explicit
--device davinci0 --device davinci3 with privileged=true still reports device_count()==8.
GPUStack currently deploys workloads privileged, so the Device policy's isolation is nominal in
the configuration it ships in. Two causes: want_all = r_v == "all" or privileged shouldn't treat
privileged as "all cards", and even fixed, a privileged container sees every node anyway.

Smaller: the continue at docker.py:1145 is not an isolation hole — I measured that the device
nodes alone bound device_count() — but it does lose NUMA affinity (cpuset_cpus/cpuset_mems are
empty on the running container). And _GENERATORS_MAP has no NVIDIA/Cambricon/MThreads entry, so
Device there injects nothing and starts a GPU-less container with no error.

What I could not test

No UB fabric on this host (no /dev/uburma, no /dev/ummu, spod-info unsupported, Main Board
0x6c(4P)), so this says nothing about the UB argument either way. Since EI0014 explains the
failure completely without invoking UB, it may be worth re-checking whether UB was ever involved —
and gating any surviving A5-specific behaviour on real UB presence rather than arch_family == 950,
since this host is an A5 that is not in a super pod.

thxCode added a commit that referenced this pull request Sep 6, 2026
Supersedes #25. yxf0314's diagnosis was right that multi-card A5 needed a
change; this takes the part of it that holds up, and drops the rest.

A5 loads libhccl_v2.so, which accepts a 2.0 ranktable only. Ranktable format is
bound to the chip generation -- 1.0 for A2, 1.2 for A3, 2.0 for A5 -- so a table
left over from an older fleet is refused, not ignored: HCCL fails with
Config_Error_Ranktable(EI0014) and multi-card init never happens. Isolated on an
8x Ascend950PR host: mounting only /etc/hccl_rootinfo.json into an otherwise
working container reproduces EI0014, mounting only driver/topo does not.

Two things follow, and nothing else does.

GPUStack stops mounting the file on A5. This is its own mount list only, so it
covers the CDI path. Older generations keep the file, which is correct for them.

Under the default Env policy the mount is ascend-docker-runtime's -- addUBMount
is a bare os.Stat with no version check -- so GPUStack cannot prevent it and
reporting is the only available action. A one-shot warning names EI0014 and says
the file must be absent or 2.0. An absent file is the healthy host and stays
silent; so does an older generation with its own table, since only the A5 row of
the generation mapping has a measured failure behind it.

This is defence, not repair: with the host file moved aside, unmodified
gpustack-runtime already runs GLM-5.3-Flash TP=8 under the default policy.
What it stops is a node's leftover ranktable silently killing multi-card
workloads on it.

Deliberately not included, having been measured and found unnecessary:

  A Device injection policy. It adds a configuration knob that does nothing by
  default, on the Env path where the vendor runtime does the mounting -- so it
  would not protect anyone who had not already been told to configure it. Its
  isolation is also nominal in the shape GPUStack deploys in, since a privileged
  container sees every device node whatever it was granted.

  Mounting the whole /usr/local/Ascend/driver for A5. driver/ube_mgmt is a
  staging directory for upgrade-tool holding one zero-byte lock file; no library
  under driver/lib64 references it, and the urma symbols are already inside the
  lib64 mount. The tree would also carry upgrade-tool and hccn_tool into a
  container holding /dev/davinci_manager, plus the CA store and 2570 kernel
  sources.
thxCode added a commit that referenced this pull request Sep 6, 2026
* fix(ascend): stop A5 from receiving a ranktable its HCCL rejects

Supersedes #25. yxf0314's diagnosis was right that multi-card A5 needed a
change; this takes the part of it that holds up, and drops the rest.

A5 loads libhccl_v2.so, which accepts a 2.0 ranktable only. Ranktable format is
bound to the chip generation -- 1.0 for A2, 1.2 for A3, 2.0 for A5 -- so a table
left over from an older fleet is refused, not ignored: HCCL fails with
Config_Error_Ranktable(EI0014) and multi-card init never happens. Isolated on an
8x Ascend950PR host: mounting only /etc/hccl_rootinfo.json into an otherwise
working container reproduces EI0014, mounting only driver/topo does not.

Two things follow, and nothing else does.

GPUStack stops mounting the file on A5. This is its own mount list only, so it
covers the CDI path. Older generations keep the file, which is correct for them.

Under the default Env policy the mount is ascend-docker-runtime's -- addUBMount
is a bare os.Stat with no version check -- so GPUStack cannot prevent it and
reporting is the only available action. A one-shot warning names EI0014 and says
the file must be absent or 2.0. An absent file is the healthy host and stays
silent; so does an older generation with its own table, since only the A5 row of
the generation mapping has a measured failure behind it.

This is defence, not repair: with the host file moved aside, unmodified
gpustack-runtime already runs GLM-5.3-Flash TP=8 under the default policy.
What it stops is a node's leftover ranktable silently killing multi-card
workloads on it.

Deliberately not included, having been measured and found unnecessary:

  A Device injection policy. It adds a configuration knob that does nothing by
  default, on the Env path where the vendor runtime does the mounting -- so it
  would not protect anyone who had not already been told to configure it. Its
  isolation is also nominal in the shape GPUStack deploys in, since a privileged
  container sees every device node whatever it was granted.

  Mounting the whole /usr/local/Ascend/driver for A5. driver/ube_mgmt is a
  staging directory for upgrade-tool holding one zero-byte lock file; no library
  under driver/lib64 references it, and the urma symbols are already inside the
  lib64 mount. The tree would also carry upgrade-tool and hccn_tool into a
  container holding /dev/davinci_manager, plus the CA store and 2570 kernel
  sources.

* fixup! fix(ascend): stop A5 from receiving a ranktable its HCCL rejects
@yxf0314 yxf0314 closed this Sep 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants